/**
 * Copyright [2012-2013] [Guðmundur Björn Birkisson]
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. Copyright [2012-2013] [Guðmundur Björn Birkisson]
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * ---------------------------------------------------------------------------
 * 
 * Class name:  RDParser
 * 
 * Description of class:
 * 
 * Recursive Decent parser using JFlex with two passes for the Karel language.
 * 
 */
package karel.lang;

import java.io.IOException;
import java.io.Reader;
import java.util.UUID;
import karel.UI.DebugWindow;
import karel.lang.newParserTools.RDActivationRecordCounter;
import karel.lang.newParserTools.RDGoCounter;
import karel.lang.secondpass.FunArg;
import karel.lang.secondpass.FunCall;
import karel.lang.secondpass.FunDecl;
import karel.lang.secondpass.IfEnd;
import karel.lang.secondpass.IfMid;
import karel.lang.secondpass.IfStart;
import karel.lang.secondpass.Literal;
import karel.lang.secondpass.ParamDecl;
import karel.lang.secondpass.Program;
import karel.lang.secondpass.RepeatFormatInject;
import karel.lang.secondpass.Return;
import karel.lang.secondpass.StepInject;
import karel.lang.secondpass.VarAssign;
import karel.lang.secondpass.VarCall;
import karel.lang.secondpass.VarDecl;
import karel.lang.secondpass.WhileEnd;
import karel.lang.secondpass.WhileMid;
import karel.lang.secondpass.WhileStart;

public class RDParser {

    public String yylval;
    private Yylex lexer;
    private Yytoken currentToken;
    public StringBuffer asmCode;
    public StringBuffer warnings;
    private boolean isDebugging;
    
    private Program currentProgram;
    private int nestingDepth;
    

    public RDParser(Reader r) {
        asmCode = new StringBuffer();
        warnings = new StringBuffer();
        lexer = new Yylex(r, this);
        isDebugging = false;
        RDGoCounter.reset();
        RDActivationRecordCounter.reset();
        nestingDepth = 0;
    }
    
    public RDParser(Reader r, boolean debug) {
        this(r);
        this.isDebugging = debug;
    }
    
    public String getGeneratedCode() {
        return asmCode.toString();
    }
    
    public String getWarnings() {
        return warnings.toString();
    }

    public void parse() throws java.io.IOException {
        firstPass();
        secondPass();
        if(DebugWindow.debugMode()) {
            System.out.println("---------- RDParser Tree ----------");
            currentProgram.printTree();
            System.out.println("-----------------------------------");
        }
    }
    
    private void firstPass() throws IOException {
        consume(); //Get the first Token of the Lexer
        currentProgram = new Program(0,0);
        program();
    }
    
    private void secondPass() throws IOException {
        currentProgram.generate(this);
    }

    public void error(String msg, int expressionLength) throws java.io.IOException {
        throw new RDParserException(msg,lexer.getLine(),lexer.getColumn(),expressionLength);
    }
    
    public void warning(String msg) {
        warnings.append(msg).append("\n");
    }
    
    //Parser Core Functions
    
    private Yytoken next() {
        return currentToken;
    }
    
    private Yytoken consume() throws IOException {
        yylval = "";
        currentToken = lexer.yylex();
        return currentToken;
    }
    
    private String expect(Yytoken expected, RDParserException e) throws IOException {
        String res = yylval;
        if(next() == expected || (next().equals(Yytoken.EOF) && expected.equals(Yytoken.NL))) {
            consume();
        }
        else {
            if(e != null) {
                throw e;
            }
            expextError(expected.toString());
        }
        return res;
    }
    
    private String expect(String expected, RDParserException e) throws IOException {
        String res = yylval;
        if(yylval.equals(expected)) {
            consume();
        }
        else {
            if(e != null) {
                throw e;
            }
            expextError(expected);
        }
        return res;
    }
    
    private void expextError(String expected) throws IOException {
        error("Fann '"+next()+"' átti von á '"+expected+"'.",1);
    }
    
    private void debug(String expre) {
        if(isDebugging) {
            System.out.println("<"+lexer.getLine()+","+lexer.getColumn()+"> "+expre+"(YyToken."+next().toString()+")");
        }
    }
    
    //Language functions
    
    private void program() throws IOException {
        debug("program");
        switch(next()) {
            case EOF  : //Do nothing
                break;
            case NL   : //NL, do nothing.
                consume(); program();
                break;
            case FUNC : //Function declaration
                consume(); fundecl(); expect(Yytoken.NL,null); program();
                break;
            case VAR  : //Varible definition
                consume(); vardecl(); expect(Yytoken.NL,null); program();
                break;
            case IF   : //If statement
                consume(); ifstatement(); program();
                break;
            case WHILE: //While statement
                consume(); whilestatement(); expect(Yytoken.NL,null); program();
                break;
            case REPEAT: //Repeat statement
                consume(); repeatstatement(); expect(Yytoken.NL,null); program();
                break;
            case RETURN:
                returnstatement(); expect(Yytoken.NL, null); program();
                break;
            case NAME:
                //Do not call consume at start to get the have the name in the function call.
                namecall(); expect(Yytoken.NL, new RDParserException("Segðir verða að enda á nýrri línu!", lexer.getLine(), lexer.getColumn(), yylval.length())); program(); 
                break;
            default:
                if(nestingDepth > 0          //Function body closing or conditional Statement closing
                   && next().equals(Yytoken.BRACKETCLOSE)    ) return; 
                error("Fann '"+next()+"' átti ekki von á því.",yylval.length());
        }
    }
    
    private void fundecl() throws IOException {
        debug("fundecl");
        
        int line = lexer.getLine();
        int column = lexer.getColumn();
        String name = expect(Yytoken.NAME, new RDParserException("Það verður að koma nafn á eftir lykilorðinu 'stef'", line, column, yylval.length()));
        
        Program newScope = new FunDecl(name, line,column);
        currentProgram.add(newScope);
        Program oldScope = currentProgram;
        currentProgram = newScope;
        
        int nrOfParams = 0;
        
        expect(Yytoken.PARAOPEN, new RDParserException("Það verður að vera svigi fyrir aftan nafn fallsins. T.d.: stef "+name+"()", lexer.getLine(), lexer.getColumn(), yylval.length()));              
        if(next()==Yytoken.NAME) {
            while(true) {
                //Watch out that the parameters are not named the same     
                nrOfParams++;
                
                int paramline = lexer.getLine();
                int paramcolumn = lexer.getColumn();
                String paramName = expect(Yytoken.NAME,new RDParserException("Átti von á nafni á viðfangi, ekki '"+yylval+"'.", lexer.getLine(), lexer.getColumn(), yylval.length()));
                
                newScope.add(new ParamDecl(paramName, paramline, paramcolumn));
                
                if(yylval.equals(",")) {
                    consume();
                } else {
                    break;
                }
            }               
        }
        expect(Yytoken.PARACLOSE,new RDParserException("Fann '"+yylval+"' en átti von á ')' eða nafni viðföngum á viðföngum í fallið '"+name+"'.", lexer.getLine(), lexer.getColumn(), yylval.length()));
        ((FunDecl)newScope).setNrOfParams(nrOfParams);

        body();
        currentProgram = oldScope;
        
    }
    
    private void vardecl() throws IOException {
        debug("vardecl");
        stepInject();
        int line = lexer.getLine();
        int column = lexer.getColumn();
        String name = expect(Yytoken.NAME, new RDParserException("Vantar nafn eftir lykilorðið 'breyta'!", line, column, yylval.length()));
        expect("=", new RDParserException("Vantar '=' eftir '"+name+"'. Td. breyta "+name+" = 1", lexer.getLine(), lexer.getColumn(), yylval.length())); 
        expr();
        currentProgram.add(new VarDecl(name, line, column));
    }
    
    private void ifstatement() throws IOException {
        debug("ifstatement");
        int go0 = RDGoCounter.getInstance().push();
        int go1 = RDGoCounter.getInstance().push();
        int line = lexer.getLine();
        int column = lexer.getColumn();
        stepInject();
        expr();
        currentProgram.add(new IfStart(go0, line, column));
        condBody();
        currentProgram.add(new IfMid(go0, go1, line, column));
        ifextend(go1);
        ifclose();
        currentProgram.add(new IfEnd(go1, line, column));
    }
    
    private void ifextend(int goLast) throws IOException {
        debug("ifextend");
        if(next().equals(Yytoken.NL)) consume(); //////////////////////////////////////////////////////////////////////////////ATH!!
        if(next()!=Yytoken.ELSEIF) return;
        int go2 = RDGoCounter.getInstance().push();
        int line = lexer.getLine();
        int column = lexer.getColumn();
        consume();
        stepInject();
        expr();
        currentProgram.add(new IfStart(go2, line, column));
        condBody();
        currentProgram.add(new IfMid(go2, goLast, line, column));
        ifextend(goLast);
    }
    
    private void ifclose() throws IOException {
        debug("ifclose");
        if(next()!=Yytoken.ELSE) return;
        consume();
        condBody();
    }
    
    private void whilestatement() throws IOException {
        debug("whilestatement");
        
        int go0 = RDGoCounter.getInstance().push();
        int go1 = RDGoCounter.getInstance().push();
        int line = lexer.getLine();
        int column = lexer.getColumn();
        currentProgram.add(new WhileStart(go0, line, column));
        stepInject();
        expr();
        currentProgram.add(new WhileMid(go1, line, column));
        
        condBody();
        currentProgram.add(new WhileEnd(go0, go1, line, column));
    }
    
    private void repeatstatement() throws IOException {
        debug("repeatstatement");
        int go0 = RDGoCounter.getInstance().push();
        int go1 = RDGoCounter.getInstance().push();
        int line = lexer.getLine();
        int column = lexer.getColumn();
        boolean isVar = false;
        String value;
        if(next().equals(Yytoken.NAME)) {
            value = expect(Yytoken.NAME, null);
            isVar = true;
        } else {
            try {
                value = expect(Yytoken.INT,null);  
            } catch(RDParserException e) {
                switch(next()) {
                    case BRACETOPEN:
                        throw new RDParserException("Vantar heiltölu eða breytu fyrir aftan 'endurtaka'. T.d. endurtaka 3", line, column, yylval.length());
                    case DOUBLE:
                    case STRING:
                        throw new RDParserException("Mátt aðeins nota heiltölu fasta eða breytu í endurtaka setningu.", line, column-yylval.length()+1, yylval.length());
                    case TRUE:
                    case FALSE:
                        throw new RDParserException("Mátt aðeins nota heiltölu fasta eða breytu í endurtaka setningu.", line, column, yylval.length());
                    default:
                        throw e;
                }
            }
        }
        
        String randomName = "A"+UUID.randomUUID().toString();
        if(isVar) {
            currentProgram.add(new VarCall(value, line, column));
            currentProgram.add(new RepeatFormatInject(line, column));
        } else {
            currentProgram.add(new Literal(value+".0", line, column));
        }
        currentProgram.add(new VarDecl(randomName, line, column));
        currentProgram.add(new WhileStart(go0, line, column));
        int ar = RDActivationRecordCounter.getInstance().dec();
        currentProgram.add(new VarCall(randomName, line, column));
        currentProgram.add(new FunArg(0, ar, line, column));
        currentProgram.add(new Literal("0.0", line, column));
        currentProgram.add(new FunArg(1, ar, line, column));
        currentProgram.add(new FunCall(">",2,ar,line,column));
        RDActivationRecordCounter.getInstance().inc();
        currentProgram.add(new WhileMid(go1, line, column));
        condBody();
        currentProgram.add(new VarCall(randomName, line, column));
        currentProgram.add(new FunArg(0, ar, line, column));
        currentProgram.add(new Literal("1.0", line, column));
        currentProgram.add(new FunArg(1, ar, line, column));
        currentProgram.add(new FunCall("-",2,ar,line,column));
        currentProgram.add(new VarAssign(randomName, line, column));
        currentProgram.add(new WhileEnd(go0, go1, line, column));
    }
    
    private void returnstatement() throws IOException {
        int line = lexer.getLine();
        int column = lexer.getColumn();
        consume();
        stepInject();
        expr();
        currentProgram.add(new Return(line, column));
    }

    private void namecall() throws IOException {
        debug("namecall");
        stepInject();
        int line = lexer.getLine();
        int column = lexer.getColumn();
        String name = expect(Yytoken.NAME,null);
        if(next().equals(Yytoken.OP) && yylval.equals("=")) {
            //Variable assignment
            consume();
            expr();
            currentProgram.add(new VarAssign(name, line, column));
        } else {
            //Function call
            int ar = RDActivationRecordCounter.getInstance().dec();
            int nrOfArguments = 0;
            expect(Yytoken.PARAOPEN,new RDParserException("Óskilgreind breyta eða kall á fall að nafni '"+name+"'. Vantar '('?", line, column, name.length()));
            
            if(next()!=Yytoken.PARACLOSE) {
                while(true) {
                    int paraLine = lexer.getLine();
                    int paraColumn = lexer.getColumn();
                    expr();
                    currentProgram.add(new FunArg(nrOfArguments, ar, paraLine, paraColumn));
                    nrOfArguments++;
                    if(yylval.equals(",")) {
                        consume();
                    } else {
                        break;
                    }
                }
            }
           
            expect(Yytoken.PARACLOSE,null);
            currentProgram.add(new FunCall(name, nrOfArguments, ar, line, column));
            RDActivationRecordCounter.getInstance().inc();
        }
    }
    
    private void body() throws IOException {
        debug("body");
        if(next().equals(Yytoken.NL)) consume(); //////////////////////////////////////////////////////////////////////////////ATH!!
        nestingDepth++;
        expect(Yytoken.BRACETOPEN, new RDParserException("Vantar '{' eftir falls skilgreiningu, gleymdist að opna stefið?", lexer.getLine(), lexer.getColumn(), 0));
        program();
        expect(Yytoken.BRACKETCLOSE, new RDParserException("Vantar '}', gleymdist að loka stefinu?", lexer.getLine(), lexer.getColumn(), 0));
        nestingDepth--;
    }
    
    private void expr() throws IOException {
        debug("expr");
        G();F();
    }
    
    private void condBody() throws IOException {
        debug("condBody");
        if(next().equals(Yytoken.NL)) consume(); //////////////////////////////////////////////////////////////////////////////ATH!!
        nestingDepth++;
        expect(Yytoken.BRACETOPEN, new RDParserException("Bjóst við '{' til þess að byrja lykkju. Fann '"+yylval+"'.", lexer.getLine(), lexer.getColumn(), 0));
        condHelp();
        expect(Yytoken.BRACKETCLOSE, new RDParserException("Bjóst við '}' til þess að loka lykkju. Fann '"+yylval+"'.", lexer.getLine(), lexer.getColumn(), 0));
        nestingDepth--;
    }
    
    private void condHelp() throws IOException {
        debug("condHelp");
        switch(next()) {
            case EOF  : //Do nothing
                break;
            case NL   : //NL, do nothing.
                consume(); condHelp();
                break;
            case IF   : //If statement
                consume(); ifstatement(); condHelp();
                break;
            case WHILE: //While statement
                consume(); whilestatement(); expect(Yytoken.NL,null); condHelp();
                break;
            case REPEAT: //Repeat statement
                consume(); repeatstatement(); expect(Yytoken.NL,null); condHelp();
                break;
            case NAME:
                //Do not call consume at start to get the have the name in the function call.
                namecall(); expect(Yytoken.NL, new RDParserException("Segðir verða að enda á nýrri línu!", lexer.getLine(), lexer.getColumn(), yylval.length())); condHelp(); 
                break;
            case RETURN:
                returnstatement(); expect(Yytoken.NL,null); condHelp();
                break;
            default:
                switch(next()) {
                    case FUNC:
                        error("Skilgreining á falli ekki leyfð í lykkjum!",yylval.length());
                    case VAR:
                        error("Skilgreining á breytum ekki leyfð í lykkjum!",yylval.length());
                    default:
                            if(nestingDepth > 0          //Function body closing or conditional Statement closing
                                && next().equals(Yytoken.BRACKETCLOSE)    ) return; 
                            error("Fann '"+next()+"' átti ekki von á því.",yylval.length());
                }
                // Do nothing
        }
    }
    
    private void F() throws IOException {
        if(next().equals(Yytoken.OR)) {
            int ar = RDActivationRecordCounter.getInstance().dec();
            int line = lexer.getLine();
            int column = lexer.getColumn();
            currentProgram.add(new FunArg(0, ar, lexer.getLine(), lexer.getColumn()));
            consume();
            G();F();
            currentProgram.add(new FunArg(1, ar, lexer.getLine(), lexer.getColumn()));
            currentProgram.add(new FunCall("OR",2,ar,line,column));
            RDActivationRecordCounter.getInstance().inc();
        }
    }
    
    private void G() throws IOException {
        I();H();
    }
    
    private void H() throws IOException {
        if(next().equals(Yytoken.AND)) {
            int ar = RDActivationRecordCounter.getInstance().dec();
            int line = lexer.getLine();
            int column = lexer.getColumn();
            currentProgram.add(new FunArg(0, ar, lexer.getLine(), lexer.getColumn()));
            consume();
            I();H();
            currentProgram.add(new FunArg(1, ar, lexer.getLine(), lexer.getColumn()));
            currentProgram.add(new FunCall("AND",2,ar,line,column));
            RDActivationRecordCounter.getInstance().inc();
        }
    }
    
    private void I() throws IOException {
        K();J();
    }
    
    private void J() throws IOException {
        if(next().equals(Yytoken.NE) || next().equals(Yytoken.EQ)) {
            String name = yylval;
            int ar = RDActivationRecordCounter.getInstance().dec();
            int line = lexer.getLine();
            int column = lexer.getColumn();
            currentProgram.add(new FunArg(0, ar, lexer.getLine(), lexer.getColumn()));
            consume();
            K();J();
            currentProgram.add(new FunArg(1, ar, lexer.getLine(), lexer.getColumn()));
            currentProgram.add(new FunCall(name,2,ar,line,column));
            RDActivationRecordCounter.getInstance().inc();
        }
    }
        
    private void K() throws IOException {
        M();L();
    }
    
    private void L() throws IOException {
        if(yylval.equals("<") || next().equals(Yytoken.LE)
                || yylval.equals(">")|| next().equals(Yytoken.GE)) {
            String name = yylval;
            int ar = RDActivationRecordCounter.getInstance().dec();
            int line = lexer.getLine();
            int column = lexer.getColumn();
            currentProgram.add(new FunArg(0, ar, lexer.getLine(), lexer.getColumn()));
            consume();
            M();L();
            currentProgram.add(new FunArg(1, ar, lexer.getLine(), lexer.getColumn()));
            currentProgram.add(new FunCall(name,2,ar,line,column));
            RDActivationRecordCounter.getInstance().inc();
        }
    }
    
    private void M() throws IOException {
        O();N();
    }

    private void N() throws IOException {
        if(yylval.equals("+") || yylval.equals("-")) {
            String name = yylval;
            int ar = RDActivationRecordCounter.getInstance().dec();
            int line = lexer.getLine();
            int column = lexer.getColumn();
            currentProgram.add(new FunArg(0, ar, lexer.getLine(), lexer.getColumn()));
            consume();
            O();N();
            currentProgram.add(new FunArg(1, ar, lexer.getLine(), lexer.getColumn()));
            currentProgram.add(new FunCall(name,2,ar,line,column));
            RDActivationRecordCounter.getInstance().inc();
        }
    }
    
    private void O() throws IOException {
        R();P();
    }
    
    private void P() throws IOException {
        if(yylval.equals("*") || yylval.equals("/") || yylval.equals("%")) {
            String name = yylval;
            int ar = RDActivationRecordCounter.getInstance().dec();
            int line = lexer.getLine();
            int column = lexer.getColumn();
            currentProgram.add(new FunArg(0, ar, lexer.getLine(), lexer.getColumn()));
            consume();
            O();N();
            currentProgram.add(new FunArg(1, ar, lexer.getLine(), lexer.getColumn()));
            currentProgram.add(new FunCall(name,2,ar,line,column));
            RDActivationRecordCounter.getInstance().inc();
        }
    }
    
    private void R() throws IOException {
        T();S();
    }
    
    private void S() throws IOException {
        if(yylval.equals("^")) {
            String name = yylval;
            int ar = RDActivationRecordCounter.getInstance().dec();
            int line = lexer.getLine();
            int column = lexer.getColumn();
            currentProgram.add(new FunArg(0, ar, lexer.getLine(), lexer.getColumn()));
            consume();
            T();S();
            currentProgram.add(new FunArg(1, ar, lexer.getLine(), lexer.getColumn()));
            currentProgram.add(new FunCall(name,2,ar,line,column));
            RDActivationRecordCounter.getInstance().inc();
        }
    }
    
    private void T() throws IOException {
        if(next().equals(Yytoken.PARAOPEN)) {
            consume();
            expr();
            expect(Yytoken.PARACLOSE,null);
        } else if(next().equals(Yytoken.NOT)) {
            int ar = RDActivationRecordCounter.getInstance().dec();
            int line = lexer.getLine();
            int column = lexer.getColumn();
            consume();
            T();
            currentProgram.add(new FunArg(0, ar, lexer.getLine(), lexer.getColumn()));
            currentProgram.add(new FunCall("!",1,ar,line,column));
            RDActivationRecordCounter.getInstance().inc();
        } else if(yylval.equals("-")) {
            int ar = RDActivationRecordCounter.getInstance().dec();
            int line = lexer.getLine();
            int column = lexer.getColumn();
            consume();
            T();
            currentProgram.add(new FunArg(0, ar, lexer.getLine(), lexer.getColumn()));
            currentProgram.add(new FunCall("-",1,ar,line,column));
            RDActivationRecordCounter.getInstance().inc();
        } else {
            U();
        }
    }
    
    private void U() throws IOException {
        if(next().equals(Yytoken.NAME)) {
            varorfun();
        } else {
            literal();
        }
    }
    
    private void varorfun() throws IOException {
        debug("varOrFun");
        int line = lexer.getLine();
        int column = lexer.getColumn();
        String name = expect(Yytoken.NAME,null);
        if(next().equals(Yytoken.PARAOPEN)) {
            consume();
            //Function         
            int ar = RDActivationRecordCounter.getInstance().dec();
            int nrOfArguments = 0;
            
            if(next()!=Yytoken.PARACLOSE) {
                while(true) {
                    int paraLine = lexer.getLine();
                    int paraColumn = lexer.getColumn();
                    expr();
                    currentProgram.add(new FunArg(nrOfArguments, ar, paraLine, paraColumn));
                    nrOfArguments++;
                    if(yylval.equals(",")) {
                        consume();
                    } else {
                        break;
                    }
                }
            }
           
            expect(Yytoken.PARACLOSE,null);
            currentProgram.add(new FunCall(name, nrOfArguments, ar, line, column));
            RDActivationRecordCounter.getInstance().inc();
            
            
        } else {
            //Variable
            
            currentProgram.add(new VarCall(name, line, column));
        }
    }
    
    private void literal() throws IOException {
        debug("literal");
        String intblocker = "";
        switch(next()) {
            case INT: //Integer
                intblocker = ".0";
            case DOUBLE:
            case STRING:
                currentProgram.add(new Literal(yylval+intblocker, lexer.getLine(), lexer.getColumn()));
                consume();
                break;
            case TRUE:
                currentProgram.add(new Literal("true", lexer.getLine(), lexer.getColumn()));
                consume();
                break;
            case FALSE:
                currentProgram.add(new Literal("false", lexer.getLine(), lexer.getColumn()));
                consume();
                break;
            default:
                error("Bjóst við segð, fann '"+yylval+"' (Tók: "+next()+")",yylval.length());
        }
    }
    
    //Functions for stepping

    private void stepInject() throws IOException {
        if(!isDebugging) {
            currentProgram.add(new StepInject(lexer.getLine(), lexer.getColumn()));
        }
    }
}
